package org.xliu.cs.projects.anno_for_doc.maven_plugins;

import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * Goal which touches a timestamp file.
 * , requiresDependencyResolution = ResolutionScope.COMPILE
 */
@Mojo(name = "generate-doc", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.COMPILE,
        threadSafe = true)
public class AnnotationMojo extends AbstractMojo {
    private static final Logger log = LoggerFactory.getLogger(AnnotationMojo.class);

    @Parameter(name = "scanPkgPrefix", required = true)
    private String scanPkgPrefix;

    @Parameter(name = "outputFilePath", defaultValue = "${project.basedir}/README.md")
    private String outputFilePath;

    @Parameter(name = "skipPkgs")
    private List<String> skipPkgs;

    // inject by maven
    @Parameter(defaultValue = "${project}", required = true, readonly = true)
    MavenProject currentProject;

    @Parameter(defaultValue = "${session}", readonly = true, required = true)
    private MavenSession session;

    private static Map<String, String> docPaths = new ConcurrentHashMap<>();

    public void execute() throws MojoExecutionException {
        List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects();
        boolean isLastOne = currentProject.getName().equals(sortedProjects.get(sortedProjects.size() - 1).getName());

        generateDoc();

        // 支持maven 多工程，获取所有的 maven 工程，
        if (sortedProjects.size() > 1 && isLastOne) {
            log.debug("handle pom packaging projects");
            assembly(sortedProjects);
        }

    }

    private void assembly(List<MavenProject> sortedProjects) throws MojoExecutionException {
        // the pom projects has no readme.md as they do not have source codes.
        // only handle pom packaging type maven projects.
        Map<MavenProject, TreeSet<MavenProject>> pomProjects = sortedProjects.stream()
                .filter(p -> isPomMavenProject(p.getPackaging()))
                .collect(Collectors.toMap(p -> p, p -> {
                    // use tree set to keep order
                    TreeSet<MavenProject> treeSet = new TreeSet<>(Comparator.comparing(MavenProject::getName));
                    treeSet.addAll(p.getCollectedProjects());
                    return treeSet;
                }));

        boolean process = true;
        while (process) {
            // current loop does not handle project only contains non-pom projects.
            process = false;
            Set<MavenProject> processedSubProjects = new HashSet<>();
            List<MavenProject> processedProjects = new ArrayList<>();

            for (Map.Entry<MavenProject, TreeSet<MavenProject>> entry : pomProjects.entrySet()) {
                Optional<MavenProject> subPomProjects = entry.getValue().stream().filter(p -> isPomMavenProject(p.getPackaging())).findAny();
                // pom project only have non pom projects
                if (!subPomProjects.isPresent()) {
                    process = true;
                    processedProjects.add(entry.getKey());
                    processedSubProjects.addAll(entry.getValue());

                    generateDocByPomFile(entry.getKey(), entry.getValue());
                }
            }
            // remove processed projects
            for (MavenProject project : processedProjects) {
                pomProjects.remove(project);
            }
            // remove sub-sub projects for non processed projects
            for (Set<MavenProject> value : pomProjects.values()) {
                value.removeAll(processedSubProjects);
            }
        }

        // process left projects
        for (Map.Entry<MavenProject, TreeSet<MavenProject>> entry : pomProjects.entrySet()) {
            generateDocByPomFile(entry.getKey(), entry.getValue());
        }
    }

    private void generateDocByPomFile(MavenProject project, TreeSet<MavenProject> subProjects) throws MojoExecutionException {
        String docPath = docPaths.get(project.getName());
        if (docPath == null) {
            throw new MojoExecutionException(String.format("project [%s] is not set the plugin", project.getName()));
        }
        docPath = docPath.replace("\\", "/");

        if (log.isDebugEnabled()) {
            log.debug("process project [{}], child projects [{}]", project,
                    subProjects.stream().map(MavenProject::getName).collect(Collectors.joining(",")));
        }

        try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
                Files.newOutputStream(Paths.get(docPath)), StandardCharsets.UTF_8))) {
            String description = Optional.ofNullable(project.getDescription()).orElse(project.getName());
            writer.write(String.format("# %s%n%n", description));

            for (MavenProject subProject : subProjects) {
                String subDescription = Optional.ofNullable(subProject.getDescription())
                        // if sub project description is not exist, it will inherit parent description.
                        .filter(sub -> !description.equals(sub))
                        .orElse(subProject.getName());
                String subDocPath = docPaths.get(subProject.getName());
                if (subDocPath == null) {
                    throw new MojoExecutionException(String.format("project [%s] is not set the plugin", subProject.getName()));
                }
                subDocPath = subDocPath.replace("\\", "/");

                writer.write(String.format("- [%s](%s)%n%n", subDescription, docRelativePath(docPath, subDocPath)));
            }
        } catch (IOException e) {
            throw new MojoExecutionException("Write doc occurs exception", e);
        }
    }

    /**
     * @param docPath    /basedir/README.md
     * @param subDocPath /basedir/p1/../pn/README.md
     * @return
     */
    static String docRelativePath(String docPath, String subDocPath) {
        int lastIndex = docPath.lastIndexOf("/");

        return subDocPath.substring(lastIndex + 1);
    }

    private boolean isPomMavenProject(String packaging) {
        return "pom".equals(packaging);
    }

    private void generateDoc() throws MojoExecutionException {
        try {
            if (skipPkgs == null) {
                skipPkgs = Collections.emptyList();
            }
            MavenGenerator mavenGenerator = new MavenGenerator(currentProject);
            mavenGenerator.generate(scanPkgPrefix, outputFilePath, new HashSet<>(skipPkgs));
            docPaths.put(currentProject.getName(), outputFilePath);
        } catch (IOException e) {
            throw new MojoExecutionException("annotation plugins occurs IOException", e);
        } catch (DependencyResolutionRequiredException e) {
            throw new MojoExecutionException("annotation plugins occurs DependencyResolutionRequiredException", e);
        }
    }
}
